import 'dart:io';

import 'package:base/net/method.dart';
import 'package:base/net/net_error_code.dart';
import 'package:connectivity/connectivity.dart';
import 'package:dio/dio.dart';

class Client {
  static const jsonContentType = "application/json;charset=UTF-8";
  static const textContentType = "text/plain;charset=UTF-8";
  static const formUrlEncodedContentType = "application/x-www-form-urlencoded";

  static const COMMON_UPLOAD = "files";
  static const COMMON_UPLOAD_ = "file";
  static const PHP_UPLOAD = "files[]";

  late CancelToken _defaultToken;
  late Dio _dio;
  late Map<String, CancelToken> _cancelCache;

  Client({
    BaseOptions? options,
    String baseUrl = "",
    Map<String, dynamic> headers = const {},
  }) {
    if (options == null) {
      options = BaseOptions()
        ..baseUrl = baseUrl
        //连接服务器超时时间，单位是毫秒.
        ..connectTimeout = 10000
        //响应流上前后两次接受到数据的间隔，单位为毫秒。
        ..receiveTimeout = 5000
        ..contentType = formUrlEncodedContentType
        ..responseType = ResponseType.json
        ..followRedirects = false
        ..headers = headers;
    }

    _cancelCache = {};
    _defaultToken = CancelToken();

    _dio = Dio(options);
  }

// 网络请求
  Future<Response<T>> httpRequest<T>(String path,
      {Method method = Method.POST,
      dynamic data,
      Map<String, dynamic> queryParameters = const {},
      Map<String, dynamic> headers = const {},
      String? tag,
      ProgressCallback? onSendProgress,
      ProgressCallback? onReceiveProgress}) async {
    CancelToken cancelToken = _buildCancelToken(tag);
    Response<T> response;
    var connectivityResult = ConnectivityResult.none;
    try {
      connectivityResult = await (Connectivity().checkConnectivity());
    } on Exception catch (e) {
      print(e);
    }

    /// 无网络
    if (connectivityResult == ConnectivityResult.none) {
      return response = Response(
        statusCode: NetErrorCode.NET_ERROR,
        requestOptions: RequestOptions(path: path),
      );
    }

    //设置header
    Options options = Options();
    if (headers.length > 0) {
      options.headers = headers;
    }
    try {
      switch (method) {
        case Method.POST:
          response = await _dio.post<T>(path,
              data: data,
              queryParameters: queryParameters,
              options: options,
              cancelToken: cancelToken,
              onSendProgress: onSendProgress,
              onReceiveProgress: onReceiveProgress);
          break;
        case Method.GET:
          response = await _dio.get<T>(path,
              queryParameters: queryParameters,
              options: options,
              cancelToken: cancelToken,
              onReceiveProgress: onReceiveProgress);
          break;
        case Method.PUT:
          response = await _dio.put<T>(path,
              data: data,
              queryParameters: queryParameters,
              options: options,
              cancelToken: cancelToken,
              onSendProgress: onSendProgress,
              onReceiveProgress: onReceiveProgress);
          break;
        case Method.HEAD:
          response = await _dio.head<T>(
            path,
            data: data,
            queryParameters: queryParameters,
            options: options,
            cancelToken: cancelToken,
          );
          break;
        case Method.DELETE:
          response = await _dio.delete<T>(
            path,
            data: data,
            queryParameters: queryParameters,
            options: options,
            cancelToken: cancelToken,
          );
          break;
        case Method.PATCH:
          response = await _dio.patch<T>(path,
              data: data,
              queryParameters: queryParameters,
              options: options,
              cancelToken: cancelToken,
              onSendProgress: onSendProgress,
              onReceiveProgress: onReceiveProgress);
          break;
      }
    } on DioError catch (e) {
      if (e.response != null) {
        response = Response(
          statusCode: e.response?.statusCode,
          requestOptions: RequestOptions(path: path),
        );
      } else {
        response = _doError<T>(path, e);
      }
    }
    return response;
  }

  ///上传文件
  Future<Response<T>> upload<T>(String path, List<String> files,
      {Map<String, dynamic> queryParameters = const {},
      ProgressCallback? onReceiveProgress,
      ProgressCallback? onSendProgress,
      String? tag}) async {
    Map<String, dynamic> uploadParams = {};

    //参数
    if (queryParameters.length > 0) {
      queryParameters.forEach((key, value) {
        uploadParams.putIfAbsent(key, () => value);
      });
    }

    //文件
    List<MultipartFile> uploadFiles = [];

    files.forEach((path) {
      uploadFiles
          .add(MultipartFile.fromFileSync(path, filename: _getFileName(path)));
    });
    if (uploadFiles.length == 1) {
      uploadParams.putIfAbsent(COMMON_UPLOAD_, () => uploadFiles[0]);
    } else {
      uploadParams.putIfAbsent(COMMON_UPLOAD, () => uploadFiles);
    }

    //上传FORM
    FormData formData = FormData.fromMap(uploadParams);

    Response<T> response = await httpRequest(path,
        data: formData,
        onSendProgress: onSendProgress,
        onReceiveProgress: onReceiveProgress);

    return response;
  }

  /// 断点下载
  Future downloadWithChunks(
    url,
    savePath, {
    ProgressCallback? onReceiveProgress,
  }) async {
    const firstChunkSize = 102;
    const maxChunk = 3;

    int total = 0;
    var dio = Dio();
    var progress = <int>[];

    createCallback(no) {
      return (int received, _) {
        progress[no] = received;
        if (onReceiveProgress != null && total != 0) {
          onReceiveProgress(progress.reduce((a, b) => a + b), total);
        }
      };
    }

    Future<Response> downloadChunk(url, start, end, no) async {
      progress.add(0);
      --end;
      return dio.download(
        url,
        savePath + "temp$no",
        onReceiveProgress: createCallback(no),
        options: Options(
          headers: {"range": "bytes=$start-$end"},
        ),
      );
    }

    Future mergeTempFiles(chunk) async {
      File f = File(savePath + "temp0");
      IOSink ioSink = f.openWrite(mode: FileMode.writeOnlyAppend);
      for (int i = 1; i < chunk; ++i) {
        File _f = File(savePath + "temp$i");
        await ioSink.addStream(_f.openRead());
        await _f.delete();
      }
      await ioSink.close();
      await f.rename(savePath);
    }

    Response response = await downloadChunk(url, 0, firstChunkSize, 0);
    if (response.statusCode == 206) {
      total = int.parse(response.headers
          .value(HttpHeaders.contentRangeHeader)!
          .split("/")
          .last);
      int reserved = total -
          int.parse(response.headers.value(HttpHeaders.contentLengthHeader)!);
      int chunk = (reserved / firstChunkSize).ceil() + 1;
      if (chunk > 1) {
        int chunkSize = firstChunkSize;
        if (chunk > maxChunk + 1) {
          chunk = maxChunk + 1;
          chunkSize = (reserved / maxChunk).ceil();
        }
        var futures = <Future>[];
        for (int i = 0; i < maxChunk; ++i) {
          int start = firstChunkSize + i * chunkSize;
          futures.add(downloadChunk(url, start, start + chunkSize, i + 1));
        }
        await Future.wait(futures);
      }
      await mergeTempFiles(chunk);
    }
  }

//直接下载
  Future<File> download(String url, String savePath,
      {ProgressCallback? onReceiveProgress}) async {
    try {
      await _dio.download(
        url,
        savePath,
        onReceiveProgress: onReceiveProgress,
      );

      return File(savePath);
    } catch (e) {
      throw e;
    }
  }

  ///取消请求
  cancel({String? tag}) {
    if (_cancelCache.containsKey(tag)) {
      _cancelCache[tag]?.cancel();
      _cancelCache.remove(tag);
    }
  }

  ///取消所有请求
  cancelAll() {
    _cancelCache.forEach((tag, cancelToken) {
      cancelToken.cancel();
    });
    _cancelCache.clear();
  }

//得到文件名
  String _getFileName(String fullPathName) {
    int index = fullPathName.lastIndexOf("/");

    return fullPathName.substring(index + 1);
  }

//错误处理
  Response<T> _doError<T>(String path, DioError e) {
    Response<T> response = Response(
      requestOptions: RequestOptions(path: path),
    );
    if (e.type == DioErrorType.connectTimeout) {
      response.statusCode = NetErrorCode.CONNECT_TIMEOUT;
    } else if (e.type == DioErrorType.cancel) {
      response.statusCode = NetErrorCode.CANCEL;
    } else if (e is SocketException) {
      response.statusCode = NetErrorCode.SOCKET_ERROR;
    } else {
      response.statusCode = NetErrorCode.HTTP_UNKNOWN_CODE;
    }

    return response;
  }

  ///构造取消tag
  CancelToken _buildCancelToken(String? tag) {
    var cancelToken;
    if (tag == null) {
      cancelToken = _defaultToken;
    } else {
      cancelToken = CancelToken();
      if (_cancelCache.containsKey(tag)) {
        _cancelCache[tag] = cancelToken;
      } else {
        _cancelCache.putIfAbsent(tag, () => cancelToken);
      }
    }
    return cancelToken;
  }
}
